%!Tex Program = xelatex
%\documentclass[a4paper]{article}
\documentclass[a4paper]{ctexart}
\usepackage{xltxtra}
\usepackage{listings}
%\setmainfont[Mapping=tex-text]{AR PL UMing CN:style=Light}
%\setmainfont[Mapping=tex-text]{AR PL UKai CN:style=Book}
%\setmainfont[Mapping=tex-text]{WenQuanYi Zen Hei:style=Regular}
%\setmainfont[Mapping=tex-text]{WenQuanYi Zen Hei Sharp:style=Regular}
%\setmainfont[Mapping=tex-text]{AR PL KaitiM GB:style=Regular} 
%\setmainfont[Mapping=tex-text]{AR PL SungtiL GB:style=Regular} 
%\setmainfont[Mapping=tex-text]{WenQuanYi Zen Hei Mono:style=Regular} 


\title{第四讲：Linux 环境和 Shell 编程 （续）}
\author{王何宇}
\date{}

\begin{document}
\maketitle
\pagestyle{empty}

\section{正则表达式（Regular Expression）}

正则表达式（Regular Expression），简称为Regex或RegExp，是一种用来匹配、
搜索和操作字符串的模式。它由一系列的字符和特殊符号组成，可以用来描述、
匹配和操作文本数据。

使用正则表达式可以进行字符串的模式匹配和搜索、替换、提取等操作。它不仅仅用于
shell，而且广泛应用于文本处理、数据清洗、表单验证、日志分析等领域。正则表达式类似通配符，
但更强大。

以下是一些常见的正则表达式元字符和符号的含义：

\begin{itemize}
\item \verb|^|：一行的开始。
\item \verb|$|：一行的结束。
\item \verb|.|：匹配任意单个字符（除了换行符）。
\item \verb|*|：匹配前面的字符零次或多次。
\item \verb|+|：匹配前面的字符至少一次或多次。
\item \verb|?|：匹配前面的字符零次或一次。
\item \verb|||：匹配两个或多个模式之一。
\item \verb|[]|：匹配方括号中的任意一个字符。
\item \verb|()|：标记子表达式的开始和结束位置，并以分组的形式进行匹配。
\item \verb|\|：转义符号，可以用来匹配特殊字符。
\end{itemize}

我们要测试正则表达式，可以构建一个测试文件，
比如 \verb|test_string|，里面是各种用于检测的文本，然后用
\begin{lstlisting}[language=sh]
grep -E "pattern" test_string
\end{lstlisting}
来检查哪些内容被匹配，这里 \verb|-E| 表示扩展的元字符。
其中 \verb|pattern| 是正则表达式。需要用单引号或双引号括起来确保安全。比如
\verb|^Ha| 表示以 \verb|Ha| 开始的行，我们可以用来测试文件 \verb|harry|：
\begin{lstlisting}[language=sh]
grep -E "^Ha" harry
\end{lstlisting}
得到全部以 \verb|Ha| 打头的行。而
\begin{lstlisting}[language=sh]
grep -E "?$" harry
\end{lstlisting}
则是全部以 \verb|?| 结束的行。所以
\begin{lstlisting}[language=sh]
grep -E "^Ca.*?$" harry
\end{lstlisting}
就是以 Ca 开始，以 ? 结束的全部行，考虑到这是 Harry Potter 的剧本，
所以这样可以找到全部 Casper 问的问题。

在工作中，正则表达式是重要的，比如，让用户输入电子邮件，
然后判定它输入的是否是一个合法的电子邮件地址：
\begin{lstlisting}[language=sh]
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
\end{lstlisting}
这里 \verb|a-z| 表示范围，所以解释就是以小写字母，大写字母，数字，和特殊符号
（\verb|._%+-| 在 email 地址的第一段都是允许的），至少要一个符号，可以有若干个，
然后是一个 \verb|@|，然后可以是小写字母，大写字母，数字，和特殊符号（\verb|.-|）一个至若干个，
然后必须以一个顶级域名为结束，也就是这里的 \verb|\.[a-zA-Z]{2,}$| 部分，
\verb|\.| 是转义字符，强制匹配一个点，然后是大小写数字构成的顶级域名，\verb|{2,}|
表示至少 2 个。

以上规范基于互联网专门用于 email 地址的 RFC 5322 规范。可见正则表达式既可以当作过滤器，
本身也可以当做规范定义形式。从这个规范我们可以看到，这样的 email 地址是合法的：
\verb|.@zju.edu.cn| 尽管在实际中没有人会这么用。一般正经的 email 地址，
可以要求以数字或字母开头，这样正则表达式就可以写成：
\begin{lstlisting}[language=sh]
^[a-zA-Z0-9][a-zA-Z0-9._%+-]*@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
\end{lstlisting}
这个大家自己体会一下。注意正则表达式显然不唯一。(\verb|test_email.sh|)

实际上我们很难（也没必要）
记住全部正则表达式的元字符，有很多工具可以帮助我们。比如身份证：
\begin{lstlisting}[language=sh]
  ^[1-9][0-9]{5}(19|20)[0-9]{2}(0[1-9]|1[0-2])
  (0[1-9]|[1-2][0-9]|3[0-1])[0-9]{3}[0-9xX]$
\end{lstlisting}
这里开始 6 位数字表示地区编码，其中首位不能有 0 ，年份是 19 或 20（建国后不许成精），
月份两位表示，要么 0 加 1-9，要么 1 加 0-2，日期则是 0 加 1-9 或 1-2 加 0-9或
3 加 0|1，再 3 位数字，最后一位允许大小写 x (居然有大小写 x？)或数字。
当然这个不能说完全没有漏洞，比如它没有排斥出生日期为：2099 年 2 月 31 日。可以进一步改进，
但会搞得太复杂。
(\verb|test_idcard.sh|)

实际上正则表达式的规范比较混乱，不论是我们的参考书，还是 GPT，经常会有错误的模板提供，
所以这是一件需要现场测试的工作。

写一个浙江大学 22 级非国际生学号的模板？
\begin{lstlisting}[language=sh]
^322010[0-9]{4}$
\end{lstlisting}

参考书的 67 页给出了一些特殊模板，所以学号的模板也可以写成
\begin{lstlisting}[language=sh]
^322010[[:digit:]]{4}$
\end{lstlisting}
注意这里 \verb|[[ ... ]]| 是双括号，但属实有点大可不必了。但有几个模板蛮好用的，比如
\verb|[[:punct:]]|，表示全部标点符号。但要注意不包括中文标点。（这也是个巨坑...）

最后，简单地验证一下不长的模板，可以用
\begin{lstlisting}[language=sh]
echo "...string..." | grep '...pattern...'
\end{lstlisting}
比如：
\begin{lstlisting}[language=sh]
echo "Hello World!" | grep '[[:punct:]]'
\end{lstlisting}

\section{\texttt{find} 命令}
\verb|find| 的基本功能是
\begin{lstlisting}[language=sh]
find [path] [options] [tests] [actions]
\end{lstlisting}
比如：
\begin{lstlisting}[language=sh]
find /usr -name "a?"
\end{lstlisting}
搜索 \verb|\usr| 目录下（递归搜索子目录），文件名起头为 a 长度为 2 的全部文件和目录。
\begin{lstlisting}[language=sh]
find /usr -name "a?" -type d
\end{lstlisting}
只搜索目录。
\begin{lstlisting}[language=sh]
find /usr -name "a?" -type d -exec cp {} ~/temp/ -r \;
\end{lstlisting}
搜出来什么都复制到指定目录下，注意最后一个是反斜杠。

\section{参数展开（Parameter Expansion）}

Parameter Expansion 使用一些特殊的语法来表示不同的操作。比如我们想遍历文件
\verb|lec01.tex|, \verb|lec02.tex|, \verb|lec03.tex|，shell 并没有数字计数器，
但我们可以这么做（方法不唯一）：
\begin{lstlisting}[language=sh]
for i in 1 2 3
do
ls -l lec0${i}.tex
done
\end{lstlisting}

下面是一些常见的 Parameter Expansion 用法：

\begin{itemize}
\item 变量的默认值：\verb|${varname:-default}| 如果 \verb|varname| 存
  在且非空，则输出变量的值；否则输出 \verb|default|。这里
  \verb|default| 是一个代替字符串，可以是变量。这里不会改变变量本身。
\item 变量的替换值：\verb|${varname:=default}| 如果 \verb|varname| 存
  在且非空，则输出变量的值；否则将变量设置为 \verb|default|，并输出
  \verb|default|。注意如果空会改变变量的值。
\item 变量的错误信息：\verb|${varname:?message}| 如果 \verb|varname|
  存在且非空，则输出变量的值；否则将打印 \verb|message| 错误信息，并退
  出脚本。
\item 变量的长度：\verb|${#varname}| 输出变量值的长度。(\verb|echo ${#myname}|)
\item 字符串截取：\verb|${varname:start:length}| 输出从 \verb|start|
  索引开始的长度为 \verb|length| 的子字符串。首符号索引是 0。（\verb|echo ${myname:0:4}|）
\item 字符串替换：\verb|${varname/pattern/replacement}|
  将 \verb|varname| 中的第一个匹配到的 \verb|pattern| 替换为 \verb|replacement|。
  \verb|pattern| 可以是正则表达式。这个可以用来替换文件名后缀。
  (\verb|echo ${myname/W???/Mo}|)
\item 字符串删除最短匹配：\verb|${varname#pattern}| 从 \verb|varname|
  开头删除最短匹配 \verb|pattern| 的部分。注意是从头开始，可以用来去掉文件名前缀。
  (\verb|filename=out.app.txt; echo ${filename#*.}|)
\item 字符串删除最长匹配：\verb|${varname##pattern}| 从 \verb|varname|
  开头删除最长匹配 \verb|pattern| 的部分。
  (\verb|filename=out.app.txt; echo ${filename#*.}|)
\item 字符串删除最短匹配（从结尾）：\verb|${varname%pattern}| 从 \verb|varname|
  结尾删除最短匹配 \verb|pattern| 的部分。
\item 字符串删除最长匹配（从结尾）：\verb|${varname%%pattern}| 从
  \verb|varname| 结尾删除最长匹配 \verb|pattern| 的部分。这两种方法可以去除文件后缀名。
\end{itemize}

\section{Here Documents}

就是直接在命令行下给出多行内容。用一对标识符（自定，怪一点好）做开头结尾标志：
\begin{lstlisting}[language=sh]
  grep -E "^322010[0-9]{4}$" << CRAZYFISH
  3220100058
  3220100013
  3330103378
  3220100067
  CRAZYFISH
\end{lstlisting}

好了， shell 先讲这么多了，接下去来点技术含量。

\section{C 语言的结构}

众所周知，C 语言是需要编译的。也就是说，我们先要有 C 的源码，然后编译成二进制的中间文件，
此时，根据用户调用的库，将系统的标准库链接到用户的二进制中间文件，最后形成可执行的程序。
这当中的两个过程，分别称为”编译“(compile)和”链接“(link)。

我们用一个简单的程序演示一下。

\verb|/example/C/hello|

我们看到，对 \verb|main.c| 编译，
\begin{lstlisting}[language=sh]
gcc -o hello main.c
\end{lstlisting}
会报错，原因是找不到函数 \verb|hello()| 的可执行代码。因为 \verb|main.c| 只能通过
\verb|hello.h| 找到 \verb|void hello()| 的函数声明，因此可以做语法检查，
但是无法产生可执行文件。实际上，单纯编译这两个 C 源文件，不会有任何错误。
\begin{lstlisting}[language=sh]
gcc -c hello.c main.c
\end{lstlisting}
因为语法上它们都对，也都产生了各自的二进制版本，这个过程就是编译。
只是为了产生最终可执行程序，需要它们两个链接起来。
\begin{lstlisting}[language=sh]
gcc -o hello hello.o main.o
\end{lstlisting}
现在生成了可执行文件 \verb|hello|，它确实可以运行。

我们平时并没有太注意到这是两个阶段，是因为我们往往这样编译：
\begin{lstlisting}[language=sh]
gcc -o hello hello.c main.c
\end{lstlisting}
这样因为全部信息都全，编译器会自动完成上面两个步骤，所以我们就觉得只有一个步骤就得到了最终程序。

但是当我们面对一个正式发行的软件时，出于商业利益或者安全或者习惯，一般提供实际功能的源代码
\verb|hello.c| 不会直接交给最终用户，只要交给他们一个二进制版本 \verb|hello.o|
和一个函数的使用规范 \verb|hello.h| 就可以了。
作为用户，负责完成 \verb|main.c| 实现对 \verb|hello()| 的调用，照样可以完成编译和链接。
比如我们这里建立一个子目录 \verb|src|，将 \verb|hello.c| 移走，然后
\begin{lstlisting}[language=sh]
gcc -c main.c
gcc -o hello main.o hello.o  
\end{lstlisting}
或者
\begin{lstlisting}[language=sh]
gcc -o hello main.c hello.o  
\end{lstlisting}
都可以生成程序。

因此在开源文化盛行之前，或在商业场合，这就成为软件发布的一个常态，交给用户一个头文件，
和一个二进制中间文件，完成软件的交付。

这里先看一个问题，用户要产生可执行文件，必须知道 \verb|hello()| 的执行代码，这个有两个办法，
一个是在生成可执行文件 \verb|hello| 时，就把 \verb|hello.o| 的全部内容全部打包进去；
另一个方法是，只有在运行的一瞬间，才从指定的位置找到 \verb|hello()| 的二进制代码，
临时链接一下。这两种做法从效率上说区别不大，主要区别是生成的可执行文件的大小。
而且如果有多个程序调用了同一个库，那么显然就会在一个系统内造成大量的冗余代码复制。
所以目前主流的做法是动态链接。于是我们需要把 \verb|.o| 文件变成动态链接库文件 \verb|.so|.
具体做法是：(进入 \verb|hello.dy|)
\begin{lstlisting}[language=sh]
gcc -shared -o libhello.so hello.c
\end{lstlisting}
注意得到一个 \verb|libhello.so|，这是一个可执行文件，但不能直接执行，因为它没有
\verb|main()|，当主函数调用到 \verb|hello()| 时，需要有一个机制告诉主函数到哪里找到它，
然后执行和 \verb|hello()| 相关的部分就行了。

按照一般的规范，我们把这个 \verb|.so| 文件移动到 \verb|lib| 目录下。

现在我们在上级目录编译 \verb|main.c|，会出错，因为找不到 \verb|hello.h|。
所以我们要先告诉编译器到哪里找头文件。方法是：
\begin{lstlisting}[language=sh]
gcc -c main.c -Iinclude
\end{lstlisting}
这里 \verb|-Ipath| 参数告诉编译器到 \verb|path| 位置找头文件。
然后我们将 \verb|main.o| 转成可执行文件 \verb|hello|，并且告诉采用共享库的形式，具体是：
\begin{lstlisting}[language=sh]
gcc -o hello main.o -Llib -lhello
\end{lstlisting}
这里 \verb|-Llib| 的意思和 \verb|-Iinclude| 类似，只是指明的是库文件而不是头文件的位置。
\verb|-lhello| 则是调用一个叫 \verb|libhello.so| 的库，头尾部分可以省略。
这样我们就得到了一个可执行文件，然而，它不能运行...
出错信息是说，运行时找不到 \verb|libhello.so| 的位置。刚才我们只是给编译器 \verb|gcc|
指明了位置，但运行是 shell 管的，shell 不知道位置，所以还要再指明一次。这次需要通过环境变量指明。
这个环境变量是 \verb|$LD_LIBRARY_PATH|。我们发现目前系统里这个环境变量是空的。加进去。
\begin{lstlisting}[language=sh]
export LD_LIBRARY_PATH=/home/hywang/Projects/mathsoft/example/C/hello.dy/lib
\end{lstlisting}
这里 \verb|export| 表示加入环境变量。现在就可以执行了。

这个例子虽然小，但 C 语言的道理都是一样的。现在让我们看一个复杂一点的库。

\section{gsl}



\bibliographystyle{plain}
\bibliography{crazyfish.bib}

\end{document}
